package cn.zwx.member.svc.rpc;

import cn.zwx.base.entity.ResponseEntity;
import cn.zwx.base.exception.BizWrapException;
import cn.zwx.base.utils.ValidationUtils;
import cn.zwx.member.svc.config.SmsConfig;
import cn.zwx.member.svc.domain.bo.AuthorizationMemberBO;
import cn.zwx.member.svc.domain.bo.UserPointsBO;
import cn.zwx.member.svc.domain.constans.MemberConstans;
import cn.zwx.member.svc.domain.entity.MemberEntity;
import cn.zwx.member.svc.domain.entity.SignRewardEntity;
import cn.zwx.member.svc.domain.entity.UserPointsEntity;
import cn.zwx.member.svc.domain.entity.UserSignEntity;
import cn.zwx.member.svc.domain.enums.*;
import cn.zwx.member.svc.domain.warp.ExceptionWrap;
import cn.zwx.member.svc.manager.DistributedManager;
import cn.zwx.member.svc.rpc.dto.*;
import cn.zwx.member.svc.rpc.facade.DubboMemberService;
import cn.zwx.member.svc.rpc.vo.*;
import cn.zwx.member.svc.service.MemberService;
import cn.zwx.member.svc.service.SignRewardService;
import cn.zwx.member.svc.service.UserPointsService;
import cn.zwx.member.svc.service.UserSignService;
import cn.zwx.member.svc.utils.*;
import com.alibaba.fastjson.JSON;
import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker;
import io.github.resilience4j.ratelimiter.annotation.RateLimiter;
import org.apache.dubbo.apidocs.annotations.ApiDoc;
import org.apache.dubbo.apidocs.annotations.ApiModule;
import org.apache.dubbo.common.utils.CollectionUtils;
import org.apache.dubbo.config.annotation.DubboService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.data.redis.connection.BitFieldSubCommands;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.ObjectUtils;

import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.*;
import java.util.concurrent.TimeUnit;

/**
 * @author zhangwenxue
 * ApiModule 必要 api-doc会查找所有带有@ApiModule的bean对象
 */
@Service
@ApiModule(value = "查询会员信息相关接口", apiInterface = DubboMemberService.class)
@DubboService(version = "v1.0.0",interfaceClass = DubboMemberService.class,timeout = 8000)
public class DubboMemberServiceImpl implements DubboMemberService {

    public static final Logger logger = LoggerFactory.getLogger(DubboMemberServiceImpl.class);

    @Resource
    private MemberService memberService;
    @Resource
    private DistributedManager distributedManager;
    @Resource
    private UserSignService userSignService;
    @Resource
    private UserPointsService userPointsService;
    @Resource
    private SignRewardService signRewardService;
    @Resource
    private RedisTemplate<String,Object> redisTemplate;
    @Resource
    private AuthorizationUtil authorizationUtil;
    @Resource
    private JwtUtils jwtUtils;
    @Resource
    private SmsUtil smsUtil;

    @Resource
    private SmsConfig smsConfig;

    @Override
    @RateLimiter(name="limiterA")
    @CircuitBreaker(name="backendA")
    @ApiDoc(value = "根据用户id查询用户信息接口")
    public ResponseEntity<MemberInfoVO> queryMemberInfo( MemberInfoDTO dto) throws BizWrapException {
        // 1.验证请求参数 cn.zwx.member.svc.rpc.DubbedMemberServiceImpl
        ValidationUtils.getInstance().validate(dto);
        // 2.查询用户数据
        MemberEntity memberEntity =  memberService.selectMemberById(dto.getUserId());
        if (Objects.isNull(memberEntity)){
            throw ExceptionWrap.wrap(MemberCodeEnum.USER_INFO_NOT_EXIST_ERROR);
        }
        // 3.查询用户会员等级
        UserPointsEntity userPointsEntity = userPointsService.selectByUserId(dto.getUserId());
        // 4.返回参数封装
        MemberInfoVO memberInfoVO = new MemberInfoVO();
        BeanUtils.copyProperties(memberEntity,memberInfoVO);
        memberInfoVO.setLevel(userPointsEntity.getLevel());
        memberInfoVO.setLevelDesc(UserLevelEnum.ofValue(userPointsEntity.getLevel()));
        memberInfoVO.setPoints(userPointsEntity.getPoints());
        memberInfoVO.setNextLevelValue(userPointsEntity.getNextLevelEmpirical());
        memberInfoVO.setUserId(memberEntity.getId());
        return ResponseEntity.success(memberInfoVO);
    }

    @Override
    @ApiDoc(value = "注册用户接口")
    @Transactional(rollbackFor = Exception.class)
    public ResponseEntity<MemberRegisterVO> register( MemberRegisterDTO dto) throws BizWrapException {
        // 1.验证参数
        ValidationUtils.getInstance().validate(dto);
        // 2.查询用户是注册过
        MemberEntity memberEntity = memberService.selectMemberByMobile(dto.getMobile());
        if (Objects.nonNull(memberEntity)){
            logger.error("用户已经注册过了,请取登录：{}", JSON.toJSONString(dto));
            throw ExceptionWrap.wrap(MemberCodeEnum.HAVE_BEEN_REGISTERED_ERROR);
        }
        // 3.添加用户信息
        final MemberEntity builder = new MemberEntity();
        builder.setUsername(dto.getMobile());
        builder.setId(distributedManager.getDistributedId());
        builder.setAuthentication(AuthenticationEnum.NON_AUTHENTICATED.getCode());
        builder.setCertificateType(CertificateTypeEnum.ID_CARD.getCode());
        builder.setStatus(MemberStatusEnum.NORMAL.getCode());
        final String generateSalt = SaltUtil.generateSalt();
        builder.setSlat(generateSalt);
        builder.setPassword(SaltUtil.hashPassword(dto.getPassword(),generateSalt));
        builder.setCreateTime(new Date());
        builder.setUpdateTime(new Date());
        memberService.insert(builder);
        // 4.返回参数
        final MemberRegisterVO registerVO = new MemberRegisterVO();
        registerVO.setMemberId(builder.getId());
        return ResponseEntity.success(registerVO);
    }

    private void verifyCode(String captchaId,String verifyCode) throws BizWrapException {
        String redisKey = MemberConstans.CAPTCHA_CACHE_KEY_PREFIX+ captchaId;
        Object objVal = redisTemplate.opsForValue().get(redisKey);
        if (Objects.isNull(objVal)){
            throw ExceptionWrap.wrap(MemberCodeEnum.CAPTCHA_VERIFY_ERROR);
        }
        if (!objVal.toString().equals(verifyCode)){
            throw ExceptionWrap.wrap(MemberCodeEnum.CAPTCHA_VERIFY_ERROR);
        }

    }

    @Override
    @ApiDoc(value = "用户登录接口")
    @Transactional(rollbackFor = Exception.class)
    public ResponseEntity<LoginVO> login( LoginDTO dto) throws BizWrapException {
        //1.验证参数
        ValidationUtils.getInstance().validate(dto);
        //2.验证码验证
        this.verifyCode(dto.getCaptchaId(), dto.getVerifyCode());
        //3.验证用户合法性
        final MemberEntity memberEntity = this.verifyMember(dto);
        //4.生成token信息
        Map<String, Object> claims = new HashMap<>(8);
        claims.put("memberId",memberEntity.getId());
        claims.put("mobile",memberEntity.getMobile());
        claims.put("username",memberEntity.getUsername());
        final String token = jwtUtils.generateToken(memberEntity.getMobile(),claims);
        final String refreshToken = jwtUtils.generateRefreshToken(memberEntity.getMobile(),claims);
        //5.返回参数信息
        final MemberTokenVO memberTokenVO = new MemberTokenVO();
        memberTokenVO.setAccessToken(token);
        memberTokenVO.setRefreshToken(refreshToken);
        memberTokenVO.setExpirationTime(jwtUtils.getExpire());
        final LoginVO loginVO = new LoginVO();
        loginVO.setToken(memberTokenVO);
        final MemberBaseVO memberBaseVO = new MemberBaseVO();
        BeanUtils.copyProperties(memberEntity,memberBaseVO);
        memberBaseVO.setMemberId(memberEntity.getId());
        loginVO.setMemberBaseVO(memberBaseVO);
        return ResponseEntity.success(loginVO);
    }

    @Override
    @ApiDoc(value = "修改用户信息接口")
    @Transactional(rollbackFor = Exception.class)
    public ResponseEntity<Void> modify(ModifyUserDTO dto) throws BizWrapException {
        // 1.check request params
        ValidationUtils.getInstance().validate(dto);
        // 2.check user exist
        final MemberEntity memberEntity = memberService.selectMemberById(dto.getUserId());
        if (ObjectUtils.isEmpty(memberEntity)){
            throw ExceptionWrap.wrap(MemberCodeEnum.USER_INFO_NOT_EXIST_ERROR);
        }
        // 3.modify user info
        final MemberEntity modifyMember = new MemberEntity();
        modifyMember.setId(dto.getUserId());
        modifyMember.setUsername(dto.getUsername());
        modifyMember.setUserIcon(dto.getUserIcon());
        modifyMember.setStatus(dto.getStatus());
        modifyMember.setBirthday(dto.getBirthday());
        modifyMember.setSex(dto.getSex());
        modifyMember.setEmail(dto.getEmail());
        modifyMember.setUpdateTime(new Date());
        memberService.update(modifyMember);
        return ResponseEntity.success();
    }

    @Override
    @ApiDoc(value = "修改密码接口")
    @Transactional(rollbackFor = Exception.class)
    public ResponseEntity<Void> modifyPassword(ModifyPasswordDTO dto) throws BizWrapException {
        // 1.check request params
        ValidationUtils.getInstance().validate(dto);
        // 2.check user exist
        final MemberEntity memberEntity = memberService.selectMemberById(dto.getUserId());
        if (ObjectUtils.isEmpty(memberEntity)){
            throw ExceptionWrap.wrap(MemberCodeEnum.USER_INFO_NOT_EXIST_ERROR);
        }
        // 3.modify password
        final boolean verifyPassword = SaltUtil.verifyPassword( dto.getSourcePassword(), memberEntity.getSlat(), memberEntity.getPassword());
        if (!verifyPassword){
            throw ExceptionWrap.wrap(MemberCodeEnum.MEMBER_PASSWORD_ERROR);
        }
        final String newPassword = SaltUtil.hashPassword(dto.getTargetPassword(), memberEntity.getSlat());
        final MemberEntity modifyMember = new MemberEntity();
        modifyMember.setPassword(newPassword);
        modifyMember.setUpdateTime(new Date());
        modifyMember.setId(dto.getUserId());
        memberService.update(memberEntity);
        return ResponseEntity.success();
    }

    @Override
    @ApiDoc(value = "忘记密码接口")
    @Transactional(rollbackFor = Exception.class)
    public ResponseEntity<Void> resetPassword(RestPasswordDTO dto) throws BizWrapException {
        // 1.check request params
        ValidationUtils.getInstance().validate(dto);
        // 2.check user exist
        final MemberEntity memberEntity = memberService.selectMemberByMobile(dto.getMobile());
        if (ObjectUtils.isEmpty(memberEntity)){
            throw ExceptionWrap.wrap(MemberCodeEnum.USER_INFO_NOT_EXIST_ERROR);
        }
        String redisKey = MemberConstans.SMS_KEY_PREFIX+memberEntity.getId();
        final Object verifyCodeObj = redisTemplate.opsForValue().get(redisKey);
        if (ObjectUtils.isEmpty(verifyCodeObj)){
            throw ExceptionWrap.wrap(MemberCodeEnum.VERIFY_CODE_GO_OUT_ERROR);
        }
        if (!Objects.equals(verifyCodeObj.toString(),dto.getVerifyCode())){
            throw ExceptionWrap.wrap(MemberCodeEnum.VERIFY_CODE_ERROR);
        }
        // 3.modify user password
        final MemberEntity modifyEntity = new MemberEntity();
        final String slat = memberEntity.getSlat();
        final String hashPassword = SaltUtil.hashPassword(dto.getTargetPassword(), slat);
        modifyEntity.setPassword(hashPassword);
        modifyEntity.setId(memberEntity.getId());
        modifyEntity.setUpdateTime(new Date());
        memberService.update(modifyEntity);
        // 4.return obj..
        return ResponseEntity.success();
    }

    @Override
    @ApiDoc(value = "用户实名认证接口")
    @Transactional(rollbackFor = Exception.class)
    public ResponseEntity<Void> authentication(AuthenticationDTO dto) throws BizWrapException {
       // 1.参数认证
        ValidationUtils.getInstance().validate(dto);
       // 2.查询用户是否实名认证
        final MemberEntity memberEntity = memberService.selectMemberById(dto.getUserId());
        if (ObjectUtils.isEmpty(memberEntity)){
            throw ExceptionWrap.wrap(MemberCodeEnum.USER_INFO_NOT_EXIST_ERROR);
        }
        // 3.认证通过直接返回
        if (Objects.equals(AuthenticationEnum.AUTHENTICATED.getCode(),memberEntity.getAuthentication())){
            return ResponseEntity.success();
        }
        final AuthorizationMemberBO authorization = authorizationUtil.authorization(dto.getIdCard(), dto.getRealName());
        if (!authorization.getResultCode().equals(MemberConstans.INT_1)){
            throw ExceptionWrap.wrap(MemberCodeEnum.NOT_AUTHORIZATION_ERROR);
        }
        // 4.更新db
        memberEntity.setUpdateTime(new Date());
        memberEntity.setAuthentication(AuthenticationEnum.AUTHENTICATED.getCode());
        memberEntity.setCertificateNo(dto.getIdCard());
        memberService.update(memberEntity);
        return ResponseEntity.success();
    }

    @Override
    @ApiDoc(value = "发送通知短信接口")
    public ResponseEntity<Void> sendMsg(SendMsgDTO dto) throws BizWrapException {
        // 1.参数认证
        ValidationUtils.getInstance().validate(dto);
        // 2.验证用户是否存在
        final MemberEntity memberEntity = memberService.selectMemberByMobile(dto.getMobile());
        if (ObjectUtils.isEmpty(memberEntity)){
            throw ExceptionWrap.wrap(MemberCodeEnum.USER_INFO_NOT_EXIST_ERROR);
        }
        if (!Objects.equals(memberEntity.getStatus(),MemberStatusEnum.NORMAL.getCode())){
            throw ExceptionWrap.wrap(MemberCodeEnum.USER_STATE_ERROR);
        }
        // 3.发送短信验证吗
        final List<SmsConfig.TemplateInfo> templateInfoList = smsConfig.getTemplates();
        if (CollectionUtils.isEmpty(templateInfoList)){
            throw ExceptionWrap.wrap(MemberCodeEnum.SEND_TEMPLATE_CONFIGURATION_ERROR);
        }
        final SmsConfig.TemplateInfo templateInfo = templateInfoList.get(0);
        final String generatePassword = GenerateStrUtil.generatePassword(MemberConstans.INT_5);
        String code = String.format(templateInfo.getTemp(),generatePassword);
        final Boolean sendSmsStat = smsUtil.sendSms(dto.getMobile(), code, templateInfo.getId());
        if (!sendSmsStat){
            throw ExceptionWrap.wrap(MemberCodeEnum.SEND_MSG_ERROR);
        }
        // 4.保存短信结果
        String redisKey = MemberConstans.SMS_KEY_PREFIX+memberEntity.getId();
        redisTemplate.opsForValue().set(redisKey,generatePassword,MemberConstans.INT_10.longValue(),TimeUnit.MINUTES);
        return ResponseEntity.success();
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    @ApiDoc(value = "用户签到接口")
    public ResponseEntity<Void> userSign(UserSignDTO dto) throws BizWrapException {
        // 1.验证参数
        ValidationUtils.getInstance().validate(dto);
        // 2.获取用户信息
        final MemberEntity memberEntity = memberService.selectMemberById(dto.getUserId());
        if (Objects.isNull(memberEntity)){
            throw ExceptionWrap.wrap(MemberCodeEnum.USER_INFO_NOT_EXIST_ERROR);
        }
        // 3.获取签到使用的
        final LocalDateTime localDateTime = LocalDateTime.now();
        final int offset = localDateTime.getDayOfMonth() - 1;
        final String redisKey = createSignKey(localDateTime,dto.getUserId());
        // 4.check是否已经签到
        final Boolean isSign  = redisTemplate.opsForValue().getBit(redisKey, offset);
        if (Boolean.TRUE.equals(isSign)){
            throw ExceptionWrap.wrap(MemberCodeEnum.ALREADY_SIGN_ERROR);
        }
        // 5.保存签到结果
        final UserSignEntity userSignEntity = new UserSignEntity();
        userSignEntity.setDate(new Date());
        userSignEntity.setYear(localDateTime.getYear());
        userSignEntity.setMoth(localDateTime.getMonthValue());
        userSignEntity.setUserId(dto.getUserId());
        userSignEntity.setIsBackup(SignBackupEnum.NORMAL.getCode());
        userSignService.insert(userSignEntity);
        // 6. 写入redis setbit key offset 1
        final Boolean bitRes = redisTemplate.opsForValue().setBit(redisKey, offset, true);
        logger.info(String.format("%s签到成功，签到结果：%s", JSON.toJSONString(localDateTime.format(DateTimeFormatter.ofPattern("yyyyMM"))),
                JSON.toJSONString(bitRes)));
        // 6.签到成功新增积分奖励
        this.processSignReward(dto);
        return ResponseEntity.success();
    }

    public void processSignReward(UserSignDTO dto) throws BizWrapException {
        final UserSignCountDTO userSignCountDTO = new UserSignCountDTO();
        userSignCountDTO.setUserId(dto.getUserId());
        final ResponseEntity<UserSignCountVO> voResponseEntity = this.userSignCount(userSignCountDTO);
        final UserSignCountVO entityData = voResponseEntity.getData();
        final SignRewardEntity signReward = signRewardService.getSignReward(entityData.getSignCount());
        final UserPointsBO userPointsBO = new UserPointsBO();
        userPointsBO.setUserId(dto.getUserId());
        List<UserPointsBO.UserPointsDetailBO> detailList = new ArrayList<>();
        final UserPointsBO.UserPointsDetailBO detailBO = new UserPointsBO.UserPointsDetailBO();
        detailBO.setPointsValue(signReward.getNumber().longValue());
        detailBO.setPointsTypeDesc("签到奖励");
        detailBO.setPointsType(1);
        detailList.add(detailBO);
        userPointsBO.setDetailList(detailList);
        userPointsService.addUserPoint(userPointsBO);
    }

    @Override
    @ApiDoc(value = "查询本月用户签到次数接口")
    @Transactional(rollbackFor = Exception.class)
    public ResponseEntity<UserSignCountVO> userSignCount(UserSignCountDTO dto) throws BizWrapException {
        // 1.验证参数
        ValidationUtils.getInstance().validate(dto);
        // 2.获取签到的key
        final LocalDateTime localDateTime = LocalDateTime.now();
        final String signKey = createSignKey(localDateTime,dto.getUserId());
        // 3.获取本月截至今天为止的所有的签到记录
        // bitfield user:sign:5:202105 u14 0 ，u 表示无符号 ，14 表示今天是14号，0 表示索引，即从第一天开始
        BitFieldSubCommands bitFieldSubCommands = BitFieldSubCommands.create().get(BitFieldSubCommands.BitFieldType.unsigned(localDateTime.getDayOfMonth()))
                .valueAt(0);
        final List<Long> resultList  = redisTemplate.opsForValue().bitField(signKey, bitFieldSubCommands);
        // 4.返回参数处理
        final UserSignCountVO userSignCountVO = new UserSignCountVO();
        userSignCountVO.setSignCount(MemberConstans.LONG_0.intValue());
        if (CollectionUtils.isEmpty(resultList)){
            return ResponseEntity.success(userSignCountVO);
        }
        Long num  = resultList.get(0);
        if (ObjectUtils.isEmpty(num) || num.compareTo(MemberConstans.LONG_0) == 0){
            return ResponseEntity.success(userSignCountVO);
        }
        int count = 0;
        //如果为0，签到结束
        while ((num & 1) != 0) {
            // 让这个数字与1 做与运算，得到数字的最后一个bit位 判断这个数字是否为0
            count++;
            num >>>= 1;
        }
        userSignCountVO.setSignCount(count);
        return ResponseEntity.success(userSignCountVO);
    }

    private static String createSignKey(LocalDateTime localDateTime,String userId) {
        int offset = localDateTime.getDayOfMonth() - 1;
        String redisKey = MemberConstans.USER_SIGN_KEY_PREFIX + userId + ":"+localDateTime.format(DateTimeFormatter.ofPattern("yyyy-MM"));
        logger.info(String.format("入参：key:%s，offset:%s", JSON.toJSONString(redisKey), JSON.toJSONString(offset)));
        return redisKey;
    }

    private MemberEntity verifyMember(LoginDTO dto) throws BizWrapException {
        final MemberEntity memberEntity = memberService.selectMemberByMobile(dto.getMobile());
        if (Objects.isNull(memberEntity)){
            throw ExceptionWrap.wrap(MemberCodeEnum.USER_INFO_NOT_EXIST_ERROR);
        }
        if (MemberStatusEnum.NORMAL.getCode() != memberEntity.getStatus()){
            throw ExceptionWrap.wrap(MemberCodeEnum.USER_STATE_ERROR);
        }
        String redisKey = MemberConstans.USER_PASSWORD_ERROR_TIME_KEY_PREFIX+memberEntity.getId();
        final Object errorTime = redisTemplate.opsForValue().get(redisKey);
        if (Objects.nonNull(errorTime) && Integer.parseInt(errorTime.toString()) > MemberConstans.INT_5){
            throw ExceptionWrap.wrap(MemberCodeEnum.OVERHEAD_PASSWORD_ERROR);
        }
        final boolean verifyPassword = SaltUtil.verifyPassword(dto.getPassword(), memberEntity.getSlat(), memberEntity.getPassword());
        if (verifyPassword){
            return memberEntity;
        }
        MemberCodeEnum codeEnum = MemberCodeEnum.USER_PASSWORD_ERROR;
        String errorMsg;
        if (Objects.nonNull(errorTime)){
            int subtractExact = Math.subtractExact(MemberConstans.INT_5, Integer.parseInt(errorTime.toString())+1);
            if (subtractExact <= 0){
                errorMsg = "您今天错误次数用完了";
            }else {
                errorMsg = String.format(codeEnum.getMsg(), subtractExact);
            }
            redisTemplate.opsForValue().increment(redisKey);
        }else {
            LocalDateTime midnight = LocalDateTime.now().plusDays(1).withHour(0).withMinute(0).withSecond(0).withNano(0);
            long millSeconds = ChronoUnit.MILLIS.between(LocalDateTime.now(),midnight);
            redisTemplate.opsForValue().set(redisKey,1, millSeconds,TimeUnit.MILLISECONDS);
            int subtractExact = Math.subtractExact(MemberConstans.INT_5, 1);
            errorMsg = String.format(codeEnum.getMsg(), subtractExact);
        }
        throw new BizWrapException(codeEnum.getCode(), errorMsg);
    }

}
